Skip to content

feat: deserialize responses into typed Pydantic models - #20

Merged
calvin-archastro merged 1 commit into
mainfrom
feat/6266-response-type-deserialization
Jun 12, 2026
Merged

feat: deserialize responses into typed Pydantic models#20
calvin-archastro merged 1 commit into
mainfrom
feat/6266-response-type-deserialization

Conversation

@rob-archastro

Copy link
Copy Markdown
Contributor

What changed

Fixes firstlanding#6266: every SDK method was annotated as returning a Pydantic model but returned a raw dict at runtime — no deserialization step existed. HttpClient.request() and SyncHttpClient.request() now accept an optional response_type and validate the JSON body through a cached pydantic.TypeAdapter. Generics (TypeVar + @overload) make the type flow to call sites; hand-written callers (auth.py, channels) that omit response_type keep raw-dict behavior. A bodyless 204 on an operation promising a typed body raises ValidationError at the call. Ships py.typed (verified present in the wheel) — without it mypy treated the whole package as untyped. Includes the regenerated SDK + contract tests from the companion generator PR; contract tests now assert concrete Pydantic classes.

sequenceDiagram
    participant C as Caller
    participant R as Generated resource method
    participant H as HttpClient.request
    participant X as httpx
    C->>R: installations.activate(id)
    R->>H: request(path, response_type=Installation)
    H->>X: HTTP request
    X-->>H: response
    alt status 204 and response_type set
        H-->>C: raises ValidationError as server omitted promised body
    else response_type set
        H->>H: TypeAdapter(Installation).validate_python(json)
        H-->>C: Installation instance
    else no response_type
        H-->>C: raw parsed JSON
    end
Loading
classDiagram
    class HttpClient {
        request(path, response_type) T
        request_raw(path) dict
    }
    class SyncHttpClient {
        request(path, response_type) T
        request_raw(path) dict
    }
    class TypeAdapterCache {
        _type_adapter(tp) TypeAdapter
    }
    HttpClient ..> TypeAdapterCache : validates via
    SyncHttpClient ..> TypeAdapterCache : validates via
Loading

Scope: backend-only (Python SDK runtime + generated code).

Risk: medium — runtime breaking change for SDK users doing dict access on responses (resp["id"]resp.id); the SDK is at 0.1.1 with few consumers. Release should be a minor bump (0.2.0) with a changelog note. The validation surfaced and the companion PR fixed a latent typegen bug (object-field annotation shadowing) that made every Attachment.object value fail validation.

User impact: SDK responses become real Pydantic models — IDE autocomplete, validation, datetime coercion; mypy/pyright now type-check actual calls (verified with both).

Testing: red-first runtime tests (model / list[Model] / raw-dict / 204 both paths / ValidationError, both transports); 38 http_client tests total incl. new coverage of request assembly, token precedence, path-prefix, error parsing; 1,720 regenerated contract tests against Prism; harness + phx_channel suites; ruff clean. Full local suite: 1,799 passed.

Follow-ups: none in this repo. PyPI release timing is a separate decision.

🤖 Generated with Claude Code

…6266)

Both HttpClient and SyncHttpClient.request() now take an optional
response_type and validate the JSON body through a cached pydantic
TypeAdapter. TypeVar + overloads make the type flow to call sites:
response_type=Model infers Model, list[Model] infers the list, and
omitting it keeps the raw-dict behavior auth.py and channels rely on.
A bodyless 204 on an operation that promises a typed body raises
ValidationError instead of silently returning None.

Ships py.typed so type checkers actually read the package's
annotations (mypy previously treated the entire SDK as untyped), and
the SDK + contract tests regenerated with the generator that emits
response_type= per call (ArchAstro/archastro-openapi companion PR).
Contract tests now assert concrete Pydantic classes instead of dict
access.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rob-archastro

Copy link
Copy Markdown
Contributor Author

Companion generator PR: ArchAstro/archastro-openapi#28. Merge order: this PR first; publish the new generator version only after both land (regenerate workflow installs @latest).

@archastro

archastro Bot commented Jun 12, 2026

Copy link
Copy Markdown

✅ Clean PR, @rob-archastro! No blocking findings on 8ac7d42 — a few nice things I clocked:

  • Clean mirror of the runtime change across both transports — response_type validation, the cached _type_adapter, and the 204→ValidationError fail-loud are identical in HttpClient.request and SyncHttpClient.request, holding the sync-http-client-mirrors-async convention. ✨
  • Smart call on raising ValidationError for a bodyless 204 on a typed operation — failing at the call site instead of letting a None surface as a downstream AttributeError is the right contract-violation signal. 🎯
  • Test added that locks in every new mode — model / list[Model] / raw-dict / 204 both paths / ValidationError, both transports, plus token-precedence and error-parsing coverage. Solid red-green discipline. 🙌

Reply @archastro <verb>: review · do <pattern> · don't <pattern> · forget <slug> · list

@calvin-archastro
calvin-archastro merged commit 505b14b into main Jun 12, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants